import math
import logging
from datetime import datetime
from itertools import groupby

from odoo import api, fields, models, _, Command, SUPERUSER_ID
from odoo.exceptions import ValidationError, UserError
from odoo.tools.safe_eval import safe_eval
from odoo.tools import float_is_zero, float_compare, float_round

_logger = logging.getLogger(__name__)


class SbOrderInternalChange(models.Model):
    _name = 'sb.order.internal.change'
    _description = 'Order Internal change'
    _rec_name = "invoice_number"
    _order = 'id desc'

    def _get_pn_product_domain(self):
        return [('categ_id.code', 'in', ('CHP', 'OPFP'))]

    def _get_pn_customer_domain(self):
        customer_ids = self.env['customer.item.code'].sudo().search([('product_id.bom_ids', '!=', False)]).mapped('customer_id')
        return [('id', 'in', customer_ids.ids)]

    apply_id = fields.Many2one('sale.change.apply', string='Sale Change Apply')
    invoice_number = fields.Char(string='Number', copy=False, default='New')
    origin_product = fields.Many2one('product.product', string='Origin Product', domain=_get_pn_product_domain, required=True)
    origin_product_structure = fields.Char(string='Origin Structure', compute='_compute_origin_product_structure', store=True)

    target_product = fields.Many2one('product.product', string='Target Product', domain=_get_pn_product_domain, required=True)
    target_product_structure = fields.Char(string='Target Structure', compute='_compute_target_product_structure', store=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Done'),
    ], string='State', default='draft', copy=False)
    change_levels = fields.Many2many('internal.change.level', string='Change Levels')
    change_level = fields.Many2one('internal.change.level', string='Specify Level')
    work_productions = fields.One2many('modifiable.work.production.line', 'change_id', string='Manufacturing Orders')
    has_tochange_lines = fields.Boolean(string='Has To Change Lines', compute='_compute_has_tochange_lines', store=True)
    customer_code = fields.Char('Item Code', compute='_compute_customer_code', store=True)
    customer_products = fields.Many2many('product.product', string='Customer Products')
    customer_id = fields.Many2one('res.partner', 'Customer', domain=_get_pn_customer_domain)
    picking_ids = fields.Many2many('stock.picking', string='Stock Pickings')
    picking_count = fields.Integer(compute='_compute_picking_count', store=True, string='Stock Pickings Count')
    change_workorder = fields.Boolean(string='Change WorkOrder', default=True)
    change_material = fields.Boolean(string='Change Material')
    remarks = fields.Text('Remarks', copy=False)

    @api.onchange('target_product')
    def onchange_target_product(self):
        if self.origin_product and self.target_product:
            if not self.target_product.serial_number:
                self.target_product = False
                return {
                    'warning': {
                        'title': "Warning",
                        'message': _(
                            "Change product unavailable",
                        )
                    }
                }
            if self.origin_product.serial_number != self.target_product.serial_number:
                self.target_product = False
                return {
                    'warning': {
                        'title': "Warning",
                        'message': _(
                            "The source product and the changed product are not of the same model",
                        )
                    }
                }

    def action_pickings(self):
        action = {
            'name': _('Stock Pickings'),
            'view_mode': 'tree,form',
            'view_type': 'form',
            'res_model': 'stock.picking',
            'target': 'current',
            'type': 'ir.actions.act_window'
        }
        if self.picking_count == 1:
            action.update({
                'view_mode': 'form',
                'res_id': self.picking_ids.id
            })
        else:
            action.update({
                'domain': [('id', 'in', self.picking_ids.ids)]
            })
        return action

    @api.depends('picking_ids')
    def _compute_picking_count(self):
        for rec in self:
            rec.picking_count = len(rec.picking_ids)

    @api.onchange('apply_id')
    def onchange_apply_id(self):
        self.customer_id = self.apply_id.partner_id
        self.origin_product = self.apply_id.product_tmpl_id.product_variant_id
        self.target_product = self.apply_id.c_product_tmpl_id.product_variant_id

    # @api.onchange('customer_id')
    # def _onchange_customer_id(self):
    #     product_templates = self.env['customer.item.code'].sudo().search([
    #         ('customer_id', '=', self.customer_id.id)
    #     ]).mapped('product_tmpl_ids').filtered(lambda x: x.bom_ids)
    #     product_ids = self.env['product.product'].sudo().search([('product_tmpl_id', 'in', product_templates.ids)])
    #     self.customer_products = [(6, 0, product_ids.ids)]

    @api.depends('customer_id', 'origin_product')
    def _compute_customer_code(self):
        for rec in self:
            customer_item = self.env['customer.item.code'].search([
                ('product_id', '=', self.origin_product.product_tmpl_id.id),
                ('customer_id', '=', self.customer_id.id),
            ], limit=1)
            rec.customer_code = customer_item.code or ''

    @api.depends('work_productions')
    def _compute_has_tochange_lines(self):
        for rec in self:
            tochange_lines = rec.work_productions.filtered(lambda x: x.to_change)
            rec.has_tochange_lines = True if tochange_lines else False

    @api.onchange('origin_product_structure')
    def _onchange_origin_product_structure(self):
        change_levels = self.env['internal.change.level']
        last_level = 0
        code = 'R'
        for structure in safe_eval(self.origin_product_structure):
            if structure.get('level') != last_level:
                code += f"-{str(structure.get('index')).zfill(2)}"
            change_level = change_levels.sudo()._create_if_not_exist(code)
            change_levels |= change_level
            last_level = structure.get('level')
        self.change_levels = change_levels
        if self.change_levels not in change_levels:
            self.change_level = False

    @api.depends('origin_product')
    def _compute_origin_product_structure(self):
        for rec in self:
            productTemplate = self.env['product.template']
            origin_product_structure = productTemplate.get_bom_structure(self.origin_product, self.origin_product)
            sorted_origin_product_structure = sorted(origin_product_structure, key=lambda x: (x['level'], x['index']))
            rec.origin_product_structure = sorted_origin_product_structure

    @api.depends('target_product')
    def _compute_target_product_structure(self):
        for rec in self:
            productTemplate = self.env['product.template']
            target_product_structure = productTemplate.get_bom_structure(self.target_product, self.target_product)
            sorted_target_product_structure = sorted(target_product_structure, key=lambda x: (x['level'], x['index']))
            rec.target_product_structure = sorted_target_product_structure

    def btn_confirm(self):
        self.ensure_one()
        change_lines = self.work_productions.filtered(lambda x: x.to_change)
        if not change_lines:
            raise ValidationError(_('At least one line needs to be selected'))
        origin_product_structure = safe_eval(self.origin_product_structure)
        target_product_structure = safe_eval(self.target_product_structure)
        update_productions = self.env['mrp.production']
        # 处理对应制造订单
        for change_line in change_lines:
            # 根据产品结构获取目标bom
            production = change_line.production_id
            all_productions = production | production._get_all_child()
            # 倒序，先处理子订单
            for single_production in all_productions.sorted(lambda x: x.id, reverse=True):
                index = 0
                for item in origin_product_structure:
                    if item['bom_id'] == single_production.bom_id.id:
                        break
                    index += 1
                try:
                    target_bom_id = self.env['mrp.bom'].browse(target_product_structure[index].get('bom_id'))
                except:
                    raise ValidationError(_(f'There is no corresponding hierarchical BOM for MO:{single_production.name}'))
                if single_production.bom_id != target_bom_id:
                    update_productions = self.update_single_production(single_production, target_bom_id, update_productions, all_productions)
            # 根据制造订单、销售明细，匹配对应补投单
            # supplement_lines = self.env['s.supplement.mo.detail'].search([
            #     ('sale_order_line', 'in', production.s_sale_order_line_ids.ids),
            #     ('origin_mrp_production', 'in', production.ids),
            # ])
            # supplement_productions = supplement_lines.supplement_mrp_production
            # for supplement_production in supplement_productions:
            #     all_productions = supplement_production | supplement_production._get_all_child()
            #     # 倒序，先处理子订单
            #     for single_production in all_productions.sorted(lambda x: x.id, reverse=True):
            #         index = 0
            #         for item in origin_product_structure:
            #             if item['bom_id'] == single_production.bom_id.id:
            #                 break
            #             index += 1
            #         try:
            #             target_bom_id = self.env['mrp.bom'].browse(target_product_structure[index].get('bom_id'))
            #         except:
            #             raise ValidationError(_(f'There is no corresponding hierarchical BOM for MO:{single_production.name}'))
            #         if single_production.bom_id != target_bom_id:
            #             update_productions = self.update_single_production(single_production, target_bom_id, update_productions, all_productions)
            # 解锁销售单
            sale_order = production.s_sale_order_line_ids.order_id
            production._unlock_order(sale_order)
        # change_lines.production_id.write({
        #     'change_ids': [(4, self.id)],
        #     's_makeup_order': True,
        #     's_done_convert': False,
        #     's_makeup_product_id': self.origin_product.id,
        # })
        self.state = 'done'

        if self.change_material:
            # 判断是否需要处理板材
            diff_cr_vals = self._get_cr_material()
            if diff_cr_vals:
                all_change_production_ids = self.work_productions.filtered(lambda x: x.to_change).mapped('production_id')
                self._handle_cr_material(all_change_production_ids, diff_cr_vals)

    def _get_cr_material(self):
        self.ensure_one()
        origin_cr_vals = self.env['plan.material.requirement.wizard']._get_bom_material_total(self.origin_product.bom_ids[0], {})
        target_cr_vals = self.env['plan.material.requirement.wizard']._get_bom_material_total(self.target_product.bom_ids[0], {})
        diff_vals = {}
        for product in target_cr_vals.keys():
            if product not in origin_cr_vals.keys() and product.categ_id.parent_id.code and product.categ_id.parent_id.code == 'CR':
                diff_vals[product] = target_cr_vals[product]
        return diff_vals

    def _handle_cr_material(self, production_ids, diff_cr_vals):
        """
        处理板材
        """
        for _, p in groupby(production_ids.sorted(key=lambda x: x.base_mo), key=lambda x: x.base_mo):
            p_ids = self.env['mrp.production'].concat(*p)
            product_qty = sum(p_ids.mapped('product_uom_qty'))
            production_product = p_ids[0].product_id
            panel_pcs = p_ids[0].s_value_pnl
            sheet_panel = p_ids[0].uom_value_ids.filtered(lambda x: x.uom_id.code == 'SHEET').uom_value
            cr_qty = math.ceil(product_qty / panel_pcs / sheet_panel)  # 需要板料的数量
            all_production_ids = p_ids
            for production in p_ids:
                all_production_ids |= production._get_all_child()

            # 变更芯板材料
            self._change_cr_material(all_production_ids)
            # 计算芯板材料的用量， 创建调拨
            all_production_ids.mapped('s_material_picking_ids').filtered(lambda x: x.state != 'done').with_user(SUPERUSER_ID).with_context({'skip_mo_check': True}).action_cancel()
            self._picking_cr_material(p_ids, panel_pcs, sheet_panel)

    def _change_cr_material(self, production_ids):
        for production in production_ids:
            moves_to_unlink = self.env['stock.move']
            bom_line_vals = self._get_cr_product(production.bom_id)
            for move_raw in production.move_raw_ids:
                if not self.check_cr_material(move_raw.product_id):
                    continue
                bom_line = bom_line_vals.pop((move_raw.bom_line_id.id, move_raw.product_id.id), False)
                if not bom_line:
                    moves_to_unlink |= move_raw

            list_move_raw = []
            moves_raw_values = self._get_cr_moves_raw_values(production)
            move_raw_dict = {move.bom_line_id.id: move for move in production.move_raw_ids.filtered(lambda m: m.bom_line_id and self.check_cr_material(m.product_id))}
            for move_raw_values in moves_raw_values:
                if move_raw_values['bom_line_id'] in move_raw_dict:
                    list_move_raw += [Command.update(move_raw_dict[move_raw_values['bom_line_id']].id, move_raw_values)]
                else:
                    list_move_raw += [Command.create(move_raw_values)]
            if list_move_raw:
                production.move_raw_ids = list_move_raw

    def _picking_cr_material(self, production_ids, panel_pcs, sheet_panel):
        """
        1.计算芯板所需的数量
        2.创建芯板调拨单
        3.关联调拨单和生产单，库存移动，分摊材料消耗
        """
        if not production_ids:
            return
        if len(list(set(production_ids.mapped('base_mo')))) > 1:
            raise ValidationError('')
        bom_id = production_ids[0].bom_id
        cr_material_vals = self._get_bom_material_total(bom_id, {})
        picking_type = self.env['stock.picking.type'].search([('code', '=', 'internal'), ('barcode', '=', 'WTT-CB')], limit=1)
        product_qty_vals = {}  # 生产的数量
        move_vals_list = []  # stock.move  vals
        material_usage = {}  # 材料消耗
        all_production_ids = production_ids
        for p in production_ids:
            all_production_ids |= p._get_all_child()
        for _p in all_production_ids:
            for move in _p.move_raw_ids:
                if self.check_cr_material(move.product_id):
                    if move.product_id in product_qty_vals:
                        product_qty_vals[move.product_id] += _p.product_uom_qty
                    else:
                        product_qty_vals[move.product_id] = _p.product_uom_qty
        for product_id, qty in product_qty_vals.items():
            sheet_qty = math.ceil(qty / panel_pcs / sheet_panel)
            count = cr_material_vals.get(product_id)
            cr_total = sheet_qty * count
            rounding = product_id.uom_id.rounding
            if not float_is_zero(cr_total, precision_rounding=rounding):
                vals = {
                    'company_id': self.env.user.company_id.id,
                    'date': datetime.now(),
                    'location_id': picking_type.default_location_src_id.id,
                    'location_dest_id': picking_type.default_location_dest_id.id,
                    'product_id': product_id.id,
                    'name': product_id.display_name,
                    'procure_method': 'make_to_stock',
                    'product_uom': product_id.uom_id.id,
                    'product_before_rounding_qty': cr_total,
                    'product_uom_qty': cr_total,
                }
                material_usage[product_id] = cr_total
                move_vals_list.append((0, 0, vals))

        if move_vals_list:
            vals = {
                'company_id': self.env.user.company_id.id,
                'location_id': picking_type.default_location_src_id.id,
                'location_dest_id': picking_type.default_location_dest_id.id,
                'move_type': 'direct',
                'picking_type_id': picking_type.id,
                's_material_order': True,
                'move_ids': move_vals_list,
                'origin': ','.join(list(set(production_ids.mapped('base_mo')))) + ' / ' + production_ids[0].product_id.name
            }
            material_picking_id = self.env['stock.picking'].create(vals)
            production_ids.write({'s_material_picking_ids': [(4, material_picking_id.id, False)]})

            all_move_raw_ids = all_production_ids.mapped('move_raw_ids').filtered(lambda x: x.state not in ('done', 'cancel') and self.check_cr_material(move.product_id))
            for move in material_picking_id.move_ids:
                product = move.product_id
                # 绑定板材发料库存移动和生产库存移动
                move.write({'move_dest_ids': [(4, move.id) for move in all_move_raw_ids.filtered(lambda x: x.product_id == product)]})
            material_picking_id.sudo().action_confirm()
            # 先取消保留
            all_move_raw_ids._do_unreserve()
            # 分摊材料消耗
            for product_id, g in groupby(all_move_raw_ids.sorted(key=lambda x: x.product_id.id), key=lambda x: x.product_id):
                product_move_ids = self.env['stock.move'].concat(*g)
                if product_id not in material_usage:
                    continue

                total = material_usage[product_id]
                move_total = sum(product_move_ids.mapped('product_uom_qty'))
                rounding = product_move_ids[0].product_uom.rounding
                contrast = float_compare(total, move_total, precision_rounding=rounding)
                if contrast != 0:
                    for move in product_move_ids:
                        move.product_uom_qty = float_round(total * (move.product_uom_qty / move_total), precision_rounding=move.product_uom.rounding)
                    difference = total - sum(product_move_ids.mapped('product_uom_qty'))
                    # 分配后的有小数差异，将差值放到最后一个生产单上
                    if not float_is_zero(difference, precision_rounding=rounding):
                        product_move_ids[-1].product_uom_qty += difference

    def _get_bom_material_total(self, bom, vals):
        """
        计算芯板材料在BOM中出现的次数
        """
        if not vals:
            vals = {}

        for line in bom.bom_line_ids:
            if line.child_bom_id:
                vals = self._get_bom_material_total(line.child_bom_id, vals)
            else:
                if line.product_id in vals:
                    # 芯板材料不根据物料清单中的用量计算
                    if self.check_cr_material(line.product_id):
                        vals[line.product_id] += 1
                    else:
                        vals[line.product_id] += line.product_qty
                else:
                    if self.check_cr_material(line.product_id):
                        vals[line.product_id] = 1
                    else:
                        vals[line.product_id] = line.product_qty
        return vals

    def _get_cr_product(self, bom):
        bom_line_vals = {}
        if not bom:
            return bom_line_vals
        for line in bom.bom_line_ids:
            if self.check_cr_material(line.product_id):
                bom_line_vals[(line.id, line.product_id.id)] = line
        return bom_line_vals

    def _get_cr_moves_raw_values(self, production):
        moves_raw_values = production._get_moves_raw_values()
        cr_moves_raw_values = []
        for vals in moves_raw_values:
            product_id = vals.get('product_id')
            if not product_id:
                continue
            product_id = self.env['product.product'].browse(product_id)
            if self.check_cr_material(product_id):
                cr_moves_raw_values.append(vals)
        return cr_moves_raw_values

    @api.model
    def check_cr_material(self, product_id):
        # 判断产品是否为芯板材料
        product_id.ensure_one()
        if (product_id.categ_id and product_id.categ_id.code == 'CR') or (product_id.categ_id.parent_id and product_id.categ_id.parent_id.code == 'CR'):
            return True
        return False

    # 制造订单已完成：更新订单料号、已完成工序的工序参数、组件的bom_line_id
    def update_single_production(self, production, target_bom_id, update_productions, all_productions):
        # 更新源生产订单中组件料号
        # bom_line_id 会在更新上级订单时，根据product_id更新
        mrp_production_ids = production._get_sources()
        target_product_id = target_bom_id.product_tmpl_id.product_variant_id
        update_moves = mrp_production_ids.move_raw_ids.filtered(lambda x: x.product_id == production.product_id and x.state not in ('done', 'cancel'))

        if production.state != 'done':
            quality_alerts = self.env['quality.alert'].sudo().search([
                ('s_production_id', '=', production.id),
                ('s_workorder_id', '=', production.product_id.id)
            ])
            quality_alerts.write({
                'product_id': target_product_id.id,
                'product_tmpl_id': target_bom_id.product_tmpl_id.id,
            })
            update_moves.write({
                'product_id': target_product_id.id,
                'quantity': 0
            })
            # 更新订单料号
            production.write({
                'product_id': target_product_id.id,
                'bom_id': target_bom_id.id
            })
        if self.change_workorder:
            # 已完成的工序，更新工序参数，用物料清单上的工序替换
            bom_operation_ids = target_bom_id.operation_ids
            done_workorders = production.workorder_ids.filtered(lambda x: x.state == 'done')
            for done_workorder in done_workorders:
                operation = bom_operation_ids.filtered(lambda x: x.step == done_workorder.step)
                done_workorder.param_ids = [(5, 0)] + [(0, 0, {
                        'attribute_id': param_line.attribute_id.id,
                        'value_id': param_line.value_id.id
                    }) for param_line in operation.param_line_ids]
        # 处理组件
        if self.change_material:
            self.update_move_raw_ids(production, all_productions)
            # 处理转移单
            self.update_bom_picking_ids(production)

        # 板材需要特殊处理
        # if production not in update_productions:
        #     update_productions = self.update_cr_picking_ids(production, update_productions)
        if production.state != 'done' and self.change_workorder:
            # 未完成的工序，删除工序，按物料清单上的工序重新生成
            unlink_workorder_ids = production.workorder_ids.filtered(lambda x: x.state != 'done')
            add_operation_ids = bom_operation_ids
            if done_workorders:
                add_operation_ids = add_operation_ids.filtered(lambda x: x.step > done_workorders[len(done_workorders) - 1].step)
            workorders_list = []
            for operation in add_operation_ids:
                workorders_list.append({
                    'name': operation.name,
                    'production_id': production.id,
                    'workcenter_id': operation.workcenter_id.id,
                    'product_uom_id': production.product_uom_id.id,
                    'operation_id': operation.id,
                    'state': 'pending',
                })
            if workorders_list:
                new_workorder = self.env['mrp.workorder'].sudo().create(workorders_list)
                for unlink_workorder in unlink_workorder_ids:
                    quality_alert_ids = self.env['quality.alert'].search([('s_workorder_id', '=', unlink_workorder.id)])
                    s_scrap_ids = unlink_workorder.s_scrap_ids

                    if not quality_alert_ids and not s_scrap_ids:
                        continue

                    new_w = new_workorder.filtered(lambda x: x.step == unlink_workorder.step)
                    if not new_w:
                        new_w = new_workorder[-1]
                    else:
                        new_w = new_w[0]

                    s_scrap_ids.write({'workorder_id': new_w.id})
                    quality_alert_ids.write({'s_workorder_id': new_w.id})

            unlink_workorder_ids.sudo().unlink()
        self.update_stock_move(production, target_product_id)

        # 修改单位信息
        self.update_production_uom_value(production, target_product_id)
        production.compute_attribute()
        return update_productions

    def update_production_uom_value(self, production, target_product_id):
        for line in production.uom_value_ids:
            uom_value_ids = target_product_id.uom_value_ids.filtered(lambda x: x.uom_id == line.uom_id)
            if uom_value_ids:
                line.uom_value = uom_value_ids[0].uom_value

    # 更新成品对应移动单
    def update_stock_move(self, production, target_product_id):
        if production.state in ('done', 'cancel'):
            return
        move_finished_ids = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
        if not move_finished_ids:
            return

        move_finished_ids.with_context(prefetch_fields=False).mapped('move_line_ids').unlink()
        self.env.cr.execute("update stock_move set product_id = %s where id in %s", [target_product_id.id, tuple(move_finished_ids.ids)])
        move_dest_ids = move_finished_ids.mapped('move_dest_ids')
        if move_dest_ids:
            move_dest_ids.with_context(prefetch_fields=False).mapped('move_line_ids').unlink()
            self.env.cr.execute("update stock_move set product_id = %s where id in %s",
                                [target_product_id.id, tuple(move_dest_ids.ids)])
        # move_finished_ids.write({'product_id': target_product_id.id})

    # 处理组件：已完成明细仅更新bom_line_id，未完成则全更新；缺少项新增；多余项取消后删除
    def update_move_raw_ids(self, production, all_productions):
        moves_raw_values = production._get_moves_raw_values()
        move_raw_dict = {move.product_id.id: move for move in production.move_raw_ids.filtered(lambda m: m.bom_line_id)}
        list_move_raw = []
        product_ids = []
        for move_raw_values in moves_raw_values:
            product_id = move_raw_values['product_id']
            change_product_id = self.env['product.product'].browse(product_id)
            product_ids.append(product_id)
            # 芯板的用量计算不是根据bom用量计算的，如果是芯板则直接跳过
            if self.check_cr_material(change_product_id):
                continue
            move_line = production.move_raw_ids.filtered(lambda x: x.product_id.id == product_id)
            if product_id in move_raw_dict:
                product_production = all_productions.filtered(lambda x: x.product_id.id == product_id)
                # 半成品已完成的直接不更新
                if product_production and product_production.state == 'done':
                    continue
                if move_line.state == 'done':
                    list_move_raw += [Command.update(move_raw_dict[product_id].id, {'bom_line_id': move_raw_values.get('bom_line_id')})]
                else:
                    list_move_raw += [Command.update(move_raw_dict[product_id].id, move_raw_values)]
            elif production.state != 'done':
                list_move_raw += [Command.create(move_raw_values)]
        if list_move_raw:
            production.move_raw_ids = list_move_raw
        unlink_moves = production.move_raw_ids.filtered(lambda m: m.product_id.id not in product_ids)
        if unlink_moves:
            unlink_moves._action_cancel()
            unlink_moves.unlink()

    # 板材发料单处理:
    # def update_cr_picking_ids(self, production, update_productions):
    #     cr_pickings = production.s_material_picking_ids
    #     done_cr_pickings = cr_pickings.filtered(lambda x: x.state == 'done')
    #     # 发料单未完成，取消保留，取消单据
    #     other_cr_pickings = cr_pickings - done_cr_pickings
    #     if other_cr_pickings:
    #         move_ids = other_cr_pickings.move_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
    #         move_ids._do_unreserve()
    #         other_cr_pickings.sudo().action_cancel()
    #     total_productions = self.env['mrp.production'].search([('s_material_picking_ids', 'in', production.s_material_picking_ids.ids)])
    #     update_productions |= total_productions
    #     moves_to_assign = total_productions.move_raw_ids.filtered(lambda x: x.product_id.categ_id.parent_id.code == 'CR')
    #     # 计算差量生成对应退料入库/领料出库
    #     self.create_picking(production, moves_to_assign, done_cr_pickings.move_ids, 'cr', total_productions)
    #     return update_productions

    # 按bom发料单处理:
    def update_bom_picking_ids(self, production):
        bom_pickings = production.picking_ids - production.s_material_picking_ids
        # bom发料单已完成，更新bom_line_id
        done_bom_pickings = bom_pickings.filtered(lambda x: x.state == 'done')
        for move_line in done_bom_pickings.move_ids:
            bom_line_id = production.bom_id.bom_line_ids.filtered(lambda x: x.product_id == move_line.product_id)
            move_line.bom_line_id = bom_line_id
        # bom发料单未完成，取消保留，取消单据，并根据新bom创建单据
        other_bom_pickings = bom_pickings - done_bom_pickings
        if other_bom_pickings:
            move_ids = other_bom_pickings.move_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
            move_ids._do_unreserve()
            other_bom_pickings.sudo().action_cancel()
        moves_to_assign = production.move_raw_ids.filtered(
            lambda x: x.product_id.categ_id.parent_id.code != 'CR' and x.product_id.bom_ids)
        # 计算差量生成对应退料入库/领料出库
        self.create_picking(production, moves_to_assign, done_bom_pickings.move_ids)

    # 计算已完成调拨与新物料差量生成对应退料入库/领料出库
    def create_picking(self, production, moves_to_assign, done_move_ids):
        llck_picking_type = self.env['stock.picking.type'].search([('code', '=', 'internal'), ('barcode', '=', 'WTT-PC')], limit=1)
        tlrk_picking_type = self.env['stock.picking.type'].search([('code', '=', 'internal'), ('barcode', '=', 'WTT-TL')], limit=1)
        total_products = (moves_to_assign | done_move_ids).product_id
        move_llck_list = []
        move_tlrk_list = []
        for product in total_products:
            user_lines = moves_to_assign.filtered(lambda x: x.product_id == product)
            done_ck_lines = done_move_ids.filtered(lambda x: x.product_id == product and x.picking_type_id.barcode == 'WTT-PC')
            done_rk_lines = done_move_ids.filtered(lambda x: x.product_id == product and x.picking_type_id.barcode == 'WTT-TL')
            user_qty = sum(user_lines.mapped('product_uom_qty')) if user_lines else 0
            done_ck_qty = sum(done_ck_lines.mapped('product_uom_qty')) if done_ck_lines else 0
            done_rk_qty = sum(done_rk_lines.mapped('product_uom_qty')) if done_rk_lines else 0
            real_qty = user_qty + done_rk_qty - done_ck_qty
            product_before_rounding_qty = real_qty

            real_qty = abs(real_qty)

            bom_line_id = production.bom_id.bom_line_ids.filtered(lambda x: x.product_id == product)
            vals = {
                'company_id': self.env.user.company_id.id,
                'date': datetime.now(),
                'product_id': product.id,
                'name': product.display_name,
                'procure_method': 'make_to_stock',
                'group_id': production.procurement_group_id.id,
                'product_uom': product.uom_id.id,
                'product_uom_qty': real_qty,
                'product_before_rounding_qty': product_before_rounding_qty,
                'bom_line_id': bom_line_id.id
            }
            if real_qty == 0:
                continue
            elif real_qty > 0:
                vals.update({
                    'location_id': llck_picking_type.default_location_src_id.id,
                    'location_dest_id': llck_picking_type.default_location_dest_id.id,
                })
                move_llck_list.append((0, 0, vals))
            else:
                vals.update({
                    'location_id': tlrk_picking_type.default_location_src_id.id,
                    'location_dest_id': tlrk_picking_type.default_location_dest_id.id,
                })
                move_tlrk_list.append((0, 0, vals))
        if move_llck_list:
            vals = {
                'company_id': self.env.user.company_id.id,
                'location_id': llck_picking_type.default_location_src_id.id,
                'location_dest_id': llck_picking_type.default_location_dest_id.id,
                'move_type': 'direct',
                # 'group_id': production.procurement_group_id.id if type == 'bom' else False,
                'picking_type_id': llck_picking_type.id,
                'move_ids': move_llck_list,
                'origin': production.base_mo + ' / ' + production.product_id.name,
            }
            llck_picking = self.env['stock.picking'].create(vals)
            llck_picking.action_confirm()
            production.write({'picking_ids': [(4, llck_picking.id)]})
            self.write({'picking_ids': [(4, llck_picking.id)]})
        if move_tlrk_list:
            vals = {
                'company_id': self.env.user.company_id.id,
                'location_id': tlrk_picking_type.default_location_src_id.id,
                'location_dest_id': tlrk_picking_type.default_location_dest_id.id,
                'move_type': 'direct',
                # 'group_id': production.procurement_group_id.id if type == 'bom' else False,
                'picking_type_id': tlrk_picking_type.id,
                'move_ids': move_tlrk_list,
                'origin': production.name,
            }
            tlrk_picking = self.env['stock.picking'].create(vals)
            tlrk_picking.action_confirm()
            production.write({'picking_ids': [(4, tlrk_picking.id)]})
            self.write({'picking_ids': [(4, tlrk_picking.id)]})

    # 校验满足条件则计算MO明细
    def action_internal_change_submit(self):
        self.ensure_one()
        if not all([self.origin_product, self.target_product]):
            raise UserError(_('Change product and source product cannot be empty'))
        if self.origin_product.serial_number != self.target_product.serial_number:
            raise UserError(_('The source product and the changed product are not of the same model'))

        self._check_change_rule()
        work_productions = self.get_work_productions()
        self.write({
            'work_productions': [(5, 0)] + work_productions
        })

    def get_work_productions(self):
        changeable_work_productions = []
        unchangeable_work_productions = []
        productions = self.env['mrp.production'].sudo().search([
            ('product_id', '=', self.origin_product.id),
            ('state', 'in', ['draft', 'confirmed', 'progress']),
        ])
        if not productions:
            raise ValidationError(_('there is no manufacturing order of %s', self.origin_product.name))
        for production in productions:
            # changeable, unchangeable_reason, reason_details = self.check_production_changeable(production)
            changeable, unchangeable_reason, reason_details = True, '', []
            production_val = {
                'production_id': production.id,
                'changeable': changeable,
                'to_change': True if changeable else False,
                'unchangeable_reason': unchangeable_reason,
                'reason_details': reason_details,
            }
            if changeable:
                changeable_work_productions.append((0, 0, production_val))
            else:
                unchangeable_work_productions.append((0, 0, production_val))
        return changeable_work_productions + unchangeable_work_productions

    def check_production_changeable(self, production):
        # 内外层订单均需判断
        origin_product_structure = safe_eval(self.origin_product_structure)
        target_product_structure = safe_eval(self.target_product_structure)
        error_list = []
        error_details = []
        all_productions = production | production._get_all_child()
        all_productions = all_productions.filtered(lambda x: x.state != 'cancel')
        for single_production in all_productions:
            index = 0
            for item in origin_product_structure:
                if item['bom_id'] == single_production.bom_id.id:
                    break
                index += 1
            try:
                target_bom_id = self.env['mrp.bom'].browse(target_product_structure[index].get('bom_id'))
                target_product_id = target_bom_id.product_tmpl_id.product_variant_id
            except:
                raise ValidationError(_('There is no corresponding hierarchical BOM for MO:%s', single_production.name))
            # 已完成流程&报废流程，名称需一致；
            done_workorders = single_production.workorder_ids.filtered(lambda x: x.state == 'done')
            quality_alerts = self.env['quality.alert'].sudo().search([
                ('s_production_id', '=', single_production.id),
                ('s_workorder_id', 'in', single_production.workorder_ids.ids)
            ])
            quality_workorders = quality_alerts.s_workorder_id
            for done_workorder in (done_workorders | quality_workorders):
                operation = target_bom_id.operation_ids.filtered(lambda x: x.step == done_workorder.step)
                if not operation:
                    error_list.append(_('inconsistent name of completed workorders'))
                    error_details.append((0, 0, {
                        'mrp_production_id': single_production.id,
                        'type': 'operation',
                        'detail': _(f'operation MO:{done_workorder.name} VS BOM:无')
                    }))
                elif operation.name != done_workorder.name:
                    error_list.append(_('inconsistent name of completed workorders'))
                    error_details.append((0, 0, {
                        'mrp_production_id': single_production.id,
                        'type': 'operation',
                        'detail': _(f'operation MO:{done_workorder.name} VS BOM:{operation.name}')
                    }))
            # 校验物料：已消耗物料，新bom是否包含该物料 且 用量一致
            done_moves = single_production.move_raw_ids.filtered(lambda x: x.state == 'done')
            for done_move in done_moves:
                target_bom_line = target_bom_id.bom_line_ids.filtered(lambda x: x.product_id == done_move.product_id)
                if not target_bom_line:
                    error_list.append(_('inconsistent shipment materials'))
                    error_details.append((0, 0, {
                        'mrp_production_id': single_production.id,
                        'type': 'move',
                        'detail': _(f'{done_move.product_id.name} not in Bom')
                    }))
                if done_move.bom_line_id.product_qty != target_bom_line.product_qty:
                    error_list.append(_('inconsistent shipment materials'))
                    error_details.append((0, 0, {
                        'mrp_production_id': single_production.id,
                        'type': 'move',
                        'detail': _(f'MO:{done_move.product_id.name} qty {done_move.bom_line_id.product_qty} VS BOM:{target_bom_line.product_id.name} qty {target_bom_line.product_qty}')
                    }))
            # Bom配量需一致
            if single_production.bom_id.product_qty != target_bom_id.product_qty:
                error_list.append(_('inconsistent usage of BOM'))
                error_details.append((0, 0, {
                    'mrp_production_id': single_production.id,
                    'type': 'bom',
                    'detail': _(f'MO:{single_production.product_id.name} qty {single_production.bom_id.product_qty} VS BOM:{target_product_id.name} qty {target_bom_id.product_qty}')
                }))
        if error_list:
            return False, '\n'.join(list(set(error_list))), error_details
        else:
            return True, '', []

    # 校验是否满足升版条件
    def _check_change_rule(self):
        error_list = []
        # 条件1：结构需一致，仅判断半成品
        is_bom_same = self.compare_bom_structure(safe_eval(self.origin_product_structure), safe_eval(self.target_product_structure))
        if not is_bom_same:
            error_list.append(_('Bom Structures are different'))
        # # 条件2：单位一致 PART/SET/PANEL
        # error_uoms = self.compare_product_uom(self.origin_product, self.target_product)
        # if error_uoms:
        #     error_list.append(f'{error_uoms}' + _('error'))
        if error_list:
            raise ValidationError('\n'.join(error_list))

    def compare_product_uom(self, origin_product, target_product):
        uom_list = ['PCS', 'SET', 'PANEL']
        error_uoms = []
        for uom in uom_list:
            origin_uom = origin_product.uom_value_ids.filtered(lambda x: x.uom_id.code == uom)
            target_uom = target_product.uom_value_ids.filtered(lambda x: x.uom_id.code == uom)
            if not origin_uom or not target_uom or origin_uom.uom_value != target_uom.uom_value or not origin_uom.uom_value:
                error_uoms.append(uom)
        return error_uoms

    def compare_bom_structure(self, origin_product_structure, target_product_structure):
        """对比两个BOM层级结构是否一致"""
        if len(origin_product_structure) != len(target_product_structure):
            return False
        for a, b in zip(origin_product_structure, target_product_structure):
            if (a['level'] != b['level'] or a['index'] != b['index'] or a['line_count'] != b['line_count']):
                return False
        return True

    @api.model
    def create(self, vals):
        if vals.get('invoice_number', 'New') == 'New':
            vals['invoice_number'] = self.env['ir.sequence'].next_by_code('sb.order.internal.change') or ''
        res = super(SbOrderInternalChange, self).create(vals)
        return res

    def unlink(self):
        if self.filtered(lambda x: x.state == 'done'):
            raise UserError(_('Delete in completed status is not allowed'))

        return super().unlink()
